﻿/* 2012/9/6 */
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows.Forms;
using System.IO;

namespace Cosmos.Windows.Forms.FileList
{
    /// <summary>
    /// Represents a Windows list view control, which displays a collection of file names.
    /// </summary>
    [DefaultEvent("SelectedChanged")]
    public partial class FileNameListView : UserControl, IFileList
    {
        /// <summary>
        /// Represents an item in FileNameListView control.
        /// </summary>
        class FileNameListViewItem : ListViewItem, IFileListItem
        {
            /// <summary>
            /// The path represented by current item.
            /// </summary>
            private string _filePath;

            /// <summary>
            /// Initializes a new instance of FileNameListViewItem.
            /// </summary>
            /// <param name="path">The path represented by current item.</param>
            public FileNameListViewItem(string path)
            {
                FilePath = path;
            }

            /// <summary>
            /// Gets or sets the path represented by current item.
            /// </summary>
            public string FilePath
            {
                get
                {
                    return _filePath;
                }

                set
                {
                    if (!String.IsNullOrEmpty(value) && !value.Equals(_filePath))
                    {
                        _filePath = value;
                        Text = Path.GetFileName(value);
                    }
                }
            }
        }

        #region Fields

        /// <summary>
        /// The items of fileThumbListView are being selected by some method, and the event 'SelectedIndexChanged' should do nothing.
        /// </summary>
        private bool _isSelectingListView;
        /// <summary>
        /// Prepare to rename a file. When the theItem in listView is renamed, event 'SelectedIndexChanged' should do nothing.
        /// </summary>
        private bool _isRenamingFile;

        /// <summary>
        /// The file paths which can be retrieved.
        /// </summary>
        private List<string> _pathList;

        /// <summary>
        /// The hash set which contains added paths.
        /// </summary>
        private HashSet<string> _pathSet;

        /// <summary>
        /// The item cache of list view using virtual mode.
        /// </summary>
        private Dictionary<string, FileNameListViewItem> _itemCache;

        #endregion

        public FileNameListView()
        {
            InitializeComponent();

            listView.VirtualMode = true;
            listView.VirtualListSize = 0;
            listView.View = View.List;
            listView.HideSelection = false;

            _pathList = new List<string>();
            _pathSet = new HashSet<string>();
            _itemCache = new Dictionary<string, FileNameListViewItem>();

            _isSelectingListView = false;
            _isRenamingFile = false;
        }

        #region FileList Methods

        public void SelectPrevious()
        {
            IsSelectingListView = true;
            if (listView.SelectedIndices.Count == 0)
            {
                int listSize = Count;
                if (listSize == 0)
                    return;

                listView.SelectedIndices.Add(listSize - 1);
                listView.EnsureVisible(listSize - 1);
            }
            else
            {
                int fileIndex = listView.SelectedIndices[0];
                listView.SelectedIndices.Clear();
                int listSize = Count;
                fileIndex = (--fileIndex + listSize) % listSize;
                listView.SelectedIndices.Add(fileIndex);
                listView.EnsureVisible(fileIndex);
            }
            IsSelectingListView = false;

            OnSelectedChanged(EventArgs.Empty);
        }

        public void SelectNext()
        {
            IsSelectingListView = true;
            if (listView.SelectedIndices.Count == 0)
            {
                int listSize = Count;
                if (listSize == 0)
                    return;

                listView.SelectedIndices.Add(0);
                listView.EnsureVisible(0);
            }
            else
            {
                int fileIndex = listView.SelectedIndices[0];
                listView.SelectedIndices.Clear();
                int listSize = Count;
                fileIndex = (++fileIndex) % listSize;
                listView.SelectedIndices.Add(fileIndex);
                listView.EnsureVisible(fileIndex);
            }
            IsSelectingListView = false;

            OnSelectedChanged(EventArgs.Empty);
        }

        public void SelectNone()
        {
            IsSelectingListView = true;
            listView.BeginUpdate();
            listView.SelectedIndices.Clear();
            listView.EndUpdate();
            IsSelectingListView = false;

            OnSelectedChanged(EventArgs.Empty);
        }

        public void SelectAll()
        {
            IsSelectingListView = true;
            listView.BeginUpdate();
            int listSize = Count;
            for (int i = 0; i < listSize; i++)
            {
                listView.SelectedIndices.Add(i);
            }
            listView.EndUpdate();
            IsSelectingListView = false;

            OnSelectedChanged(EventArgs.Empty);
        }

        public void SelectInvert()
        {
            IsSelectingListView = true;
            listView.BeginUpdate();
            int listSize = Count;
            for (int i = 0; i < listSize; i++)
            {
                if (listView.SelectedIndices.Contains(i))
                    listView.SelectedIndices.Remove(i);
                else
                    listView.SelectedIndices.Add(i);
            }
            listView.EndUpdate();
            IsSelectingListView = false;

            OnSelectedChanged(EventArgs.Empty);
        }

        public int GetSelectedIndex()
        {
            if (listView.SelectedIndices.Count != 0)
                return listView.SelectedIndices[0];
            else
                return -1;
        }

        public int[] GetSelectedIndices()
        {
            int count = listView.SelectedIndices.Count;
            int[] selectedIndices = new int[count];
            for (int i = 0; i < count; i++)
            {
                selectedIndices[i] = listView.SelectedIndices[i];
            }
            return selectedIndices;
        }

        public string GetSelectedPath()
        {
            if (listView.SelectedIndices.Count != 0)
                return _pathList[listView.SelectedIndices[0]];
            else
                return null;
        }

        public string[] GetSelectedPaths()
        {
            int count = listView.SelectedIndices.Count;
            string[] selectedPaths = new string[count];
            for (int i = 0; i < count; i++)
            {
                selectedPaths[i] = _pathList[listView.SelectedIndices[i]];
            }
            return selectedPaths;
        }

        public void SetSelectedIndex(int index)
        {
            SelectIndex(index, true);
            EnsureVisible(index);

            OnSelectedChanged(EventArgs.Empty);
        }

        public void SetSelectedIndices(int[] indices)
        {
            SelectIndices(indices, true);
            if (indices.Length > 0)
                EnsureVisible(indices[0]);

            OnSelectedChanged(EventArgs.Empty);
        }

        public void SetSelectedPath(string path)
        {
            SelectPath(path, true);
            EnsureVisible(path);

            OnSelectedChanged(EventArgs.Empty);
        }

        public void SetSelectedPaths(string[] paths)
        {
            SelectPaths(paths, true);
            if (paths.Length > 0)
                EnsureVisible(paths[0]);

            OnSelectedChanged(EventArgs.Empty);
        }

        public string GetPathAt(int index)
        {
            if (index >= 0 && index < Count)
                return _pathList[index];
            else
                return null;
        }
        public int IndexOf(string path)
        {
            int index = -1;
            int fileCount = _pathList.Count;
            for (int i = 0; i < fileCount; i++)
            {
                if (_pathList[i].Equals(path))
                {
                    index = i;
                    break;
                }
            }
            return index;
        }

        public bool Contains(string path)
        {
            return _pathSet.Contains(path);
        }

        public void Clear()
        {
            if (Count > 0)
                listView.EnsureVisible(0);
            listView.VirtualListSize = 0;

            _pathSet.Clear();
            _pathList.Clear();
            _itemCache.Clear();
            OnPathCleared(EventArgs.Empty);
        }

        public int Add(string path)
        {
            int theIndex = -1;
            if (path != null && !Contains(path))
            {
                _pathList.Add(path);
                listView.VirtualListSize++;
                theIndex = listView.VirtualListSize - 1;
                _pathSet.Add(path);
                OnPathAdded(new PathEventArgs(path));
            }
            return theIndex;
        }

        public void AddRange(IEnumerable<string> paths)
        {
            int fileCount = 0;
            List<string> addedPathList = null;
            if (paths is ICollection<string>)
            {
                fileCount = ((ICollection<string>)paths).Count;
                int capacity = _pathList.Capacity + fileCount;
                if (_pathList.Capacity < capacity)
                    _pathList.Capacity = capacity;
                addedPathList = new List<string>(fileCount);
            }
            else
            {
                addedPathList = new List<string>();
            }

            listView.BeginUpdate();
            foreach (string aPath in paths)
            {
                if (aPath != null && !Contains(aPath))
                {
                    _pathList.Add(aPath);
                    _pathSet.Add(aPath);
                    addedPathList.Add(aPath);
                }
            }
            listView.VirtualListSize = _pathList.Count;
            listView.EndUpdate();
            OnPathsAdded(new PathsEventArgs(addedPathList.ToArray()));
        }

        public int Insert(int index, string path)
        {
            int theIndex = -1;
            if ((index >= 0 && index < Count) && (path != null && !Contains(path)))
            {
                _pathList.Insert(index, path);
                listView.VirtualListSize++;
                theIndex = index;
                _pathSet.Add(path);
                OnPathInserted(new PathInsertedEventArgs(index, path));
            }
            return theIndex;
        }

        public int Remove(string path)
        {
            int theIndex = IndexOf(path);
            if (theIndex != -1)
            {
                _pathList.RemoveAt(theIndex);
                listView.VirtualListSize--;
                _pathSet.Remove(path);
                _itemCache.Remove(path);
                OnPathRemoved(new PathEventArgs(path));
            }
            return theIndex;
        }

        public int RemoveAt(int index)
        {
            int theIndex = -1;
            string path = null;
            if (index >= 0 && index < Count)
            {
                path = _pathList[index];
                _pathList.RemoveAt(index);
                listView.VirtualListSize--;
                theIndex = index;
                _pathSet.Remove(path);
                _itemCache.Remove(path);
                OnIndexRemoved(new IndexEventArgs(index));
            }
            return theIndex;
        }

        public int Rename(string srcPath, string dstPath)
        {
            int theIndex = -1;
            if (dstPath != null && !Contains(dstPath))
            {
                theIndex = IndexOf(srcPath);
                if (theIndex != -1)
                {
                    IsRenamingFile = true;
                    _pathList[theIndex] = dstPath;

                    _pathSet.Remove(srcPath);
                    _pathSet.Add(dstPath);

                    FileNameListViewItem item = null;
                    if (_itemCache.TryGetValue(srcPath, out item))
                    {
                        item.FilePath = dstPath;
                        _itemCache.Remove(srcPath);
                        _itemCache.Add(dstPath, item);
                        listView.Refresh();
                    }

                    IsRenamingFile = false;
                    OnRenamed(new RenamedEventArgs(srcPath, dstPath));
                }
            }
            return theIndex;
        }

        public void EnsureVisible(int index)
        {
            if (index >= 0 && index < Count)
            {
                listView.EnsureVisible(index);
            }
        }

        public void EnsureVisible(string path)
        {
            EnsureVisible(IndexOf(path));
        }

        private void SelectIndex(int index, bool selected)
        {
            IsSelectingListView = true;
            int listSize = Count;
            if (index >= 0 && index < listSize)
            {
                if (selected)
                    listView.SelectedIndices.Add(index);
                else
                    listView.SelectedIndices.Remove(index);
            }
            IsSelectingListView = false;
        }

        private void SelectIndices(IEnumerable<int> indices, bool selected)
        {
            if (indices == null)
                return;

            IsSelectingListView = true;
            int listSize = Count;
            listView.BeginUpdate();
            foreach (int anIndex in indices)
            {
                if (anIndex >= 0 && anIndex < listSize)
                    if (selected)
                        listView.SelectedIndices.Add(anIndex);
                    else
                        listView.SelectedIndices.Remove(anIndex);
            }
            listView.EndUpdate();
            IsSelectingListView = false;
        }

        private void SelectPath(string path, bool selected)
        {
            if (String.IsNullOrEmpty(path))
                return;

            IsSelectingListView = true;
            int listSize = Count;
            for (int i = 0; i < listSize; i++)
            {
                if (_pathList[i].Equals(path))
                {
                    if (selected)
                        listView.SelectedIndices.Add(i);
                    else
                        listView.SelectedIndices.Remove(i);

                    break;
                }
            }
            IsSelectingListView = false;
        }

        private void SelectPaths(IEnumerable<string> paths, bool selected)
        {
            if (paths == null)
                return;

            IsSelectingListView = true;
            List<string> pathList = new List<string>(paths);
            int listSize = Count;
            string currentPath = null;
            listView.BeginUpdate();
            for (int i = 0; i < listSize; i++)
            {
                currentPath = _pathList[i];
                if (pathList.Contains(currentPath))
                {
                    if (selected)
                        listView.SelectedIndices.Add(i);
                    else
                        listView.SelectedIndices.Remove(i);

                    pathList.Remove(currentPath);
                    if (pathList.Count == 0)
                        break;
                }
            }
            listView.EndUpdate();
            IsSelectingListView = false;
        }

        #endregion

        #region Event Trigger Methods

        protected void OnSelectedChanged(EventArgs e)
        {
            if (SelectedChanged != null)
                SelectedChanged(this, e);
        }

        protected void OnPathCleared(EventArgs e)
        {
            if (PathCleared != null)
                PathCleared(this, e);
        }

        protected void OnPathAdded(PathEventArgs e)
        {
            if (PathAdded != null)
                PathAdded(this, e);
        }

        protected void OnPathsAdded(PathsEventArgs e)
        {
            if (PathsAdded != null)
                PathsAdded(this, e);
        }

        protected void OnPathInserted(PathInsertedEventArgs e)
        {
            if (PathInserted != null)
                PathInserted(this, e);
        }

        protected void OnPathRemoved(PathEventArgs e)
        {
            if (PathRemoved != null)
                PathRemoved(this, e);
        }

        protected void OnIndexRemoved(IndexEventArgs e)
        {
            if (IndexRemoved != null)
                IndexRemoved(this, e);
        }

        protected void OnRenamed(RenamedEventArgs e)
        {
            if (Renamed != null)
                Renamed(this, e);
        }

        #endregion

        #region listView Event Handler Methods

        private void listView_SelectedIndexChanged(object sender, EventArgs e)
        {
            if (IsSelectingListView || IsRenamingFile)
                return;

            OnSelectedChanged(EventArgs.Empty);
        }

        private void listView_CacheVirtualItems(object sender, CacheVirtualItemsEventArgs e)
        {
            int itemCount = e.EndIndex - e.StartIndex + 1;

            for (int i = 0; i < itemCount; i++)
            {
                string filePath = _pathList[e.StartIndex + i];
                if (!_itemCache.ContainsKey(filePath))
                {
                    FileNameListViewItem item = new FileNameListViewItem(filePath);
                    _itemCache.Add(filePath, item);
                }
            }
        }

        private void listView_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
        {
            ListView self = sender as ListView;
            string filePath = _pathList[e.ItemIndex];

            if (_itemCache.ContainsKey(filePath))
            {
                FileNameListViewItem item = _itemCache[filePath];
                e.Item = item;
            }
            else
            {
                FileNameListViewItem item = new FileNameListViewItem(filePath);
                _itemCache.Add(filePath, item);
                e.Item = item;
            }
        }

        private void listView_SearchForVirtualItem(object sender, SearchForVirtualItemEventArgs e)
        {
            int listLength = _pathList.Count;
            for (int i = e.StartIndex; i < listLength - e.StartIndex; i++)
            {
                if (Path.GetFileName(_pathList[i]).Contains(e.Text))
                {
                    e.Index = i;
                    break;
                }
            }
        }

        #endregion

        #region FileList Properties

        [Browsable(false)]
        public int Count
        {
            get
            {
                return listView.Items.Count;
            }
        }

        [Browsable(false)]
        public string[] Paths
        {
            get
            {
                int itemCount = Count;
                string[] paths = new string[itemCount];
                for (int i = 0; i < itemCount; i++)
                {
                    FileNameListViewItem theItem = (FileNameListViewItem)listView.Items[i];
                    paths[i] = theItem.FilePath;
                }
                return paths;
            }
        }

        [Browsable(false)]
        [DefaultValue(-1)]
        public int SelectedIndex
        {
            get
            {
                return GetSelectedIndex();
            }
            set
            {
                int[] selectedIndices = GetSelectedIndices();
                SelectIndices(selectedIndices, false);
                SelectIndex(value, true);
                EnsureVisible(value);
                OnSelectedChanged(EventArgs.Empty);
            }
        }

        [Browsable(false)]
        [DefaultValue(new int[0])]
        public int[] SelectedIndices
        {
            get
            {
                return GetSelectedIndices();
            }
            set
            {
                int[] selectedIndices = GetSelectedIndices();
                if (value == null)
                {
                    SelectIndices(selectedIndices, false);
                }
                else
                {
                    SelectIndices(selectedIndices.Except(value), false);
                    SelectIndices(value.Except(selectedIndices), true);
                }
                OnSelectedChanged(EventArgs.Empty);
            }
        }

        [Browsable(false)]
        [DefaultValue(null)]
        public string SelectedPath
        {
            get
            {
                return GetSelectedPath();
            }
            set
            {
                string[] selectedPaths = GetSelectedPaths();
                SelectPaths(selectedPaths, false);
                SelectPath(value, true);
                EnsureVisible(value);
                OnSelectedChanged(EventArgs.Empty);
            }
        }

        [Browsable(false)]
        [DefaultValue(new string[0])]
        public string[] SelectedPaths
        {
            get
            {
                return GetSelectedPaths();
            }
            set
            {
                string[] selectedPaths = GetSelectedPaths();
                if (value == null)
                {
                    SelectPaths(selectedPaths, false);
                }
                else
                {
                    SelectPaths(selectedPaths.Except(value), false);
                    SelectPaths(value.Except(selectedPaths), true);
                }
                OnSelectedChanged(EventArgs.Empty);
            }
        }

        public string this[int index]
        {
            get
            {
                return GetPathAt(index);
            }
        }

        /// <summary>
        /// Prepare to rename a file. When the theItem in fileThumbListView is renamed, event 'SelectedIndexChanged' should do nothing.
        /// </summary>
        private bool IsRenamingFile
        {
            get { return _isRenamingFile; }
            set { _isRenamingFile = value; }
        }

        /// <summary>
        /// The items of fileThumbListView are being selected by some method, and the event 'SelectedIndexChanged' should do noting.
        /// </summary>
        private bool IsSelectingListView
        {
            get { return _isSelectingListView; }
            set { _isSelectingListView = value; }
        }

        #endregion

        #region FileList Events

        [Category("File List")]
        public event EventHandler SelectedChanged;

        [Category("File List")]
        public event EventHandler PathCleared;

        [Category("File List")]
        public event PathEventHandler PathAdded;

        [Category("File List")]
        public event PathsEventHandler PathsAdded;

        [Category("File List")]
        public event PathInsertedEventHandler PathInserted;

        [Category("File List")]
        public event PathEventHandler PathRemoved;

        [Category("File List")]
        public event IndexEventHandler IndexRemoved;

        [Category("File List")]
        public event RenamedEventHandler Renamed;

        #endregion
    }
}
